using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Net;
using System.ComponentModel;
using System.Runtime.Serialization;
using System.Security.Permissions;
using System.Globalization;
using C4F.DevKit.WebServices.Properties;
using C4F.DevKit.WebServices.Datasets;

namespace C4F.DevKit.WebServices
{
    /// <summary>
    /// Exposes search functionality on CraigsList.org
    /// </summary>
    [ToolboxItem(true)]
    public class CraigsList : C4FWebServicesBase
    {
        #region Public Functions
        /// <summary>
        /// Searches for the specified item from specified city
        /// </summary>
        /// <param name="city">Name of the city</param>
        /// <param name="itemName">Name of the item to be searched</param>
        /// <returns>List of items</returns>
        /// <exception cref="ArgumentException">The city is not specified</exception>
        /// <exception cref="CityNotSupportedException">An invalid city was entered as search parameter</exception>
        public List<CraigsListItem> Search(string city, string itemName)
        {
            //if city not specified throw the exception
            if(String.IsNullOrEmpty(city))
                throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, Resources.ErrorSpecifyArgument, "city"));

            city = city.Replace(" ", string.Empty);
            string requestUrl = "http://" + city + ".craigslist.org/search/sss?query=" + itemName + "&format=rss";

            string responseData = string.Empty;

            try
            {
                // Get the responses
                responseData = base.GetQueryResponse(requestUrl);
            }
            catch (WebException webEx)
            {
                // Check if server threw 502 error. This means invalid city was entered as search parameter
                if (webEx.Message.Contains("The remote"))
                    throw new CityNotSupportedException(Resources.CityNotSupported, city, webEx);

                // Don't handle other web exceptions
                throw;
            }

            // Parse the data and return results
            return CraigsList.ParseResponseXml(responseData);
        }

        /// <summary>
        /// Searches for the specified item from specified city and return the result in a dataset
        /// </summary>
        /// <param name="city">Name of the city</param>
        /// <param name="itemName">Name of the item to be searched</param>
        /// <returns>Dataset containing the list of items</returns>
        /// <exception cref="ArgumentException">The city is not specified</exception>
        /// <exception cref="CityNotSupportedException">An invalid city was entered as search parameter</exception>
        public DS_CraigsList Search_DS(string city, string itemName)
        {
            List<CraigsListItem> items = Search(city, itemName);
            DS_CraigsList dataset = new DS_CraigsList();
            foreach (CraigsListItem item in items)
                dataset.CraigsItem.AddCraigsItemRow(item.Title, item.Url,
                    item.Date, item.Description, item.Language);
            return dataset;
        }
        #endregion

        #region Private Functions
        /// <summary>
        /// Parses responseData and populates CraigsListItem object list
        /// </summary>
        /// <param name="responseData">Search response data</param>
        /// <returns>List of CraigsList items of search result</returns>
        private static List<CraigsListItem> ParseResponseXml(string responseData)
        {
            List<CraigsListItem> itemList = new List<CraigsListItem>();
            XmlDocument xmlDocument = new XmlDocument();
            XmlNamespaceManager nsmanager = new XmlNamespaceManager(xmlDocument.NameTable);
            nsmanager.AddNamespace("rdf", "http://purl.org/rss/1.0/");
            nsmanager.AddNamespace("taxo", "http://purl.org/rss/1.0/modules/taxonomy/");
            nsmanager.AddNamespace("dc", "http://purl.org/dc/elements/1.1/");
            nsmanager.AddNamespace("syn", "http://purl.org/rss/1.0/modules/syndication/");
            nsmanager.AddNamespace("admin", "http://webns.net/mvcb/");
            xmlDocument.LoadXml(responseData);

            // Retrieve item nodes
            XmlNodeList itemNodes = xmlDocument.DocumentElement.SelectNodes("rdf:item", nsmanager);

            if (itemNodes.Count == 0)
                return itemList;

            CraigsListItem cItem = null;
            foreach (XmlNode itemNode in itemNodes)
            {
                cItem = new CraigsListItem();
                //Populate cItem object
                cItem.Title = itemNode.SelectSingleNode("rdf:title", nsmanager) == null ? 
                    null : itemNode.SelectSingleNode("rdf:title", nsmanager).InnerText;
                cItem.Url = itemNode.SelectSingleNode("rdf:link", nsmanager) == null ? 
                    null : itemNode.SelectSingleNode("rdf:link", nsmanager).InnerText;
                cItem.Description = itemNode.SelectSingleNode("rdf:description", nsmanager) == null ? 
                    null : itemNode.SelectSingleNode("rdf:description", nsmanager).InnerText;
                cItem.Language = itemNode.SelectSingleNode("dc:language", nsmanager) == null ? 
                    null : itemNode.SelectSingleNode("dc:language", nsmanager).InnerText;
                if (itemNode.SelectSingleNode("dc:date", nsmanager) != null)
                    cItem.Date = DateTime.Parse(itemNode.SelectSingleNode("dc:date", nsmanager).InnerText, CultureInfo.InvariantCulture);

                itemList.Add(cItem);
            }
            return itemList;
        }
        #endregion
    }

    /// <summary>
    /// This class is used to store information related to CraigsList items.
    /// </summary>
    public class CraigsListItem
    {
        #region Private Members
        /// <summary>
        /// Title of the item
        /// </summary>
        private string title;
        /// <summary>
        /// URL with the details of the story
        /// </summary>
        private string url;
        /// <summary>
        /// Submit date
        /// </summary>
        private DateTime date;
        /// <summary>
        /// Item description
        /// </summary>
        private string description;
        /// <summary>
        /// Language
        /// </summary>
        private string language;
        #endregion

        #region Public Properties
        /// <summary>
        /// Title of the item
        /// </summary>
        public string Title
        {
            get { return this.title; }
            set { this.title = value; }
        }
        /// <summary>
        /// URL with the details of the story
        /// </summary>
        public string Url
        {
            get { return this.url; }
            set { this.url = value; }
        }
        /// <summary>
        /// Submit date
        /// </summary>
        public DateTime Date
        {
            get { return this.date; }
            set { this.date = value; }
        }
        /// <summary>
        /// Item description
        /// </summary>
        public string Description
        {
            get { return this.description; }
            set { this.description = value; }
        }
        /// <summary>
        /// Language
        /// </summary>
        public string Language
        {
            get { return this.language; }
            set { this.language = value; }
        }
        #endregion
    }

    /// <summary>
    /// This exception is used to notify user about invalid city input.
    /// </summary>
    [global::System.Serializable]
    public class CityNotSupportedException : Exception
    {
        private string cityName;
        /// <summary>
        /// Name of city provided as search parameter
        /// </summary>
        public string CityName
        {
            get { return this.cityName; }
            set { this.cityName = value; }
        }
        /// <summary>
        /// Initializes a new object of CityNotSupportedException class
        /// </summary>
        public CityNotSupportedException() { }
        /// <summary>
        /// Initializes a new object of CityNotSupportedException class
        /// </summary>
        /// <param name="message">The message that describes the error</param>
        public CityNotSupportedException(string message) : base(message) { }
        /// <summary>
        /// Initializes a new object of CityNotSupportedException class
        /// </summary>
        /// <param name="message">The message that describes the error</param>
        /// <param name="innerException">The exception that is the cause of the current exception</param>
        public CityNotSupportedException(string message, Exception innerException) 
            : base(message, innerException) { }
        /// <summary>
        /// Initializes a new object of CityNotSupportedException class
        /// </summary>
        /// <param name="message">The message that describes the error</param>
        /// <param name="cityName">Name of the city</param>
        public CityNotSupportedException(string message, string cityName)
            : base(message)
        {
            this.CityName = cityName;
        }
        /// <summary>
        /// Initializes a new object of CityNotSupportedException class
        /// </summary>
        /// <param name="message">The message that describes the error</param>
        /// <param name="cityName">Name of the city</param>
        /// <param name="innerException">The exception that is the cause of the current exception</param>
        public CityNotSupportedException(string message, string cityName, Exception innerException)
            : base(message, innerException)
        {
            this.CityName = cityName;
        }
        /// <summary>
        /// Initializes a new object of YouTubeException class
        /// </summary>
        /// <param name="info">The System.Runtime.Serialization.SerializationInfo that holds the serialized
        /// object data about the exception being thrown.</param>
        /// <param name="context">The System.Runtime.Serialization.StreamingContext that contains contextual
        /// information about the source or destination.</param>
        protected CityNotSupportedException(SerializationInfo info, StreamingContext context)
            : base(info, context) {
                this.CityName = info.GetString("cityName");
        }
        /// <summary>
        /// Sets the System.Runtime.Serialization.SerializationInfo
        /// with information about the exception.
        /// </summary>
        /// <param name="info">The System.Runtime.Serialization.SerializationInfo that holds the serialized
        /// object data about the exception being thrown.</param>
        /// <param name="context">The System.Runtime.Serialization.StreamingContext that contains contextual
        /// information about the source or destination.</param>
        [SecurityPermissionAttribute(SecurityAction.Demand,
            SerializationFormatter = true)]
        public override void GetObjectData(
            SerializationInfo info, StreamingContext context)
        {
            info.AddValue("cityName", this.CityName);
            base.GetObjectData(info, context);
        }
    }
}